#include <fstream>
#include <set>
#include <thread>
#include <boost/filesystem.hpp>
#include <boost/lexical_cast.hpp>

#include "BackupFilesRestorer.h"

/** Taille du buffer de communications UDP. */
#define UDP_BUFFER_SIZE 512
/** Taille d'un segment de fichier. */
#define FILE_SEGMENT_SIZE 400


namespace ip = boost::asio::ip;
namespace fs = boost::filesystem;


namespace
{

	/**
	 * Requete recue pour demander la restauration des fichiers d'un ordinateur donne.
	 */
	struct RestoreRequest
	{
		/** Identifiant de l'ordinateur demandant la restauration. */
		uint32_t computerId;
	
		RestoreRequest() :
			computerId(0)
		{}

		/**
		 * Extraire les informations de la requete depuis le buffer donne.
		 *
		 * @param buffer Buffer contenant la requete.
		 * @param bufferSize Taille du buffer.
		 */
		void read_from(const char* buffer, size_t bufferSize)
		{
			if (bufferSize < sizeof(uint32_t))
				throw std::invalid_argument("buffer is too small to contain an ID");

			memcpy(&computerId, buffer, sizeof(uint32_t));
		}
	};
	
} // end of anonymous namespace


struct BackupFilesRestorer::SegmentFileName
{
	uint32_t computerId;
	uint32_t patientId;
	std::string recordAcronym;
	uint32_t segmentNum;

	SegmentFileName() :
		computerId(0), patientId(0), segmentNum(0)
	{}

	void parse(const std::string& fileName)
	{
		static const std::set<std::string> RECORD_ACRONYMS(
			{"FEC", "FCT", "FMT", "FMN"});
			
		size_t computerIdEndPos = fileName.find_first_of('_');
		size_t patientIdEndPos;
		size_t recordAcroEndPos;

		if (computerIdEndPos == std::string::npos)
			throw std::invalid_argument("file name does not contain a computer ID");

		patientIdEndPos = fileName.find_first_of('_', computerIdEndPos+1);

		if (patientIdEndPos == std::string::npos)
			throw std::invalid_argument("file name does not contain a patient ID");

		recordAcroEndPos = fileName.find_first_of('_', patientIdEndPos+1);

		if (recordAcroEndPos == std::string::npos)
			throw std::invalid_argument("file name does not contain a record acronym");

		/* Extraire les informations depuis le nom du fichier.. */
		computerId = boost::lexical_cast<uint32_t>(fileName.substr(0,
				computerIdEndPos));
		patientId = boost::lexical_cast<uint32_t>(fileName.substr(
				computerIdEndPos+1, patientIdEndPos-computerIdEndPos-1));
		recordAcronym = fileName.substr(patientIdEndPos+1,
			recordAcroEndPos-patientIdEndPos-1);
		segmentNum = boost::lexical_cast<uint32_t>(fileName.substr(
				recordAcroEndPos+1));

		if (RECORD_ACRONYMS.find(recordAcronym) == RECORD_ACRONYMS.end())
			throw std::invalid_argument("unknown record type");
	}
};


BackupFilesRestorer::BackupFilesRestorer(const LogWriter& logWriter,
	unsigned short commPort) :
	m_logWriter(logWriter),
	m_socket(m_ioService, ip::udp::endpoint(ip::udp::v4(), commPort))
{}


void BackupFilesRestorer::set_storage_directory(
	const std::string& storageDirPathStr)
{
	m_storageDirPath = storageDirPathStr;
}


void BackupFilesRestorer::run()
{
	{
		std::stringstream ss;
		ss << "Listening on port " << m_socket.local_endpoint().port() << ".";
		m_logWriter.write_info(ss.str());
	}

	/* Rester dans l'attente de messages indefiniment. */
	while (true)
	{
		try
		{
			ip::udp::endpoint senderEndpoint;
			std::array<char, UDP_BUFFER_SIZE> buffer;
			size_t msgSize = 0;
			
			/* Attendre la reception d'un fragment de fichier. */
			msgSize = m_socket.receive_from(
				boost::asio::buffer(buffer), senderEndpoint);
			
			/* S'occuper du fragment recu. */
			handle_reception(buffer.data(), msgSize, senderEndpoint);
		}
		catch(std::exception& exc)
		{
			m_logWriter.write_error(exc.what());
		}
	}
}


void BackupFilesRestorer::handle_reception(const char* buffer, std::size_t msgSize,
	const ip::udp::endpoint& senderEndpoint)
{
	RestoreRequest request;

	request.read_from(buffer, msgSize);

	{	
		std::stringstream ss;
		ss << "Treating request from the computer with ID " << request.computerId
			<< " (" << senderEndpoint.address() << ":" << senderEndpoint.port() << ")";
		m_logWriter.write_info(ss.str());
	}

	for (fs::directory_iterator current(m_storageDirPath), last;
		 current != last;
		 ++current)
	{
		try
		{
			/* Ignorer les elements n'etant pas des fichiers reguliers. */
			if (fs::is_symlink(*current) || !fs::is_regular_file(*current))
				continue;
			
			SegmentFileName segmentFileName;

			segmentFileName.parse(current->path().filename().native());

			/* Ignorer les fichiers n'appartenant pas a l'ordinateur demandeur. */
			if (request.computerId != segmentFileName.computerId)
				continue;

			send_segment_file(current->path(), segmentFileName, senderEndpoint);
		}
		catch (std::exception& exc)
		{
			m_logWriter.write_error(exc.what());
		}
	}
}


void BackupFilesRestorer::send_segment_file(const fs::path& filePath,
	const SegmentFileName& fileNameInfo, const ip::udp::endpoint& destEndpoint)
{
	std::array<char, UDP_BUFFER_SIZE> buffer;
	char* bufferPtr = buffer.data();
	std::ifstream segmentFile(filePath.native());
	uint32_t segmentSize;
	ip::udp::socket socket(m_ioService);

	if (!segmentFile.good())
		throw std::invalid_argument("failed to open the segment file");

	/* Ecrire l'identifiant du patient. */
	memcpy(bufferPtr, &fileNameInfo.patientId, sizeof(uint32_t));
	bufferPtr += sizeof(uint32_t);

	/* Ecrire l'acronyme de la fiche. */
	memcpy(bufferPtr, fileNameInfo.recordAcronym.data(),
		fileNameInfo.recordAcronym.size());
	bufferPtr += fileNameInfo.recordAcronym.size();

	/* Ecrire le numero du segment. */
	memcpy(bufferPtr, &fileNameInfo.segmentNum, sizeof(uint32_t));
	bufferPtr += sizeof(uint32_t);

	/* Ecrire le segment lui-meme (en laissant la place pour sa taille). */
	segmentSize = segmentFile.readsome(bufferPtr+sizeof(uint32_t), FILE_SEGMENT_SIZE);

	/* Ecrire la taille du segment, avant celui-ci. */
	memcpy(bufferPtr, &segmentSize, sizeof(uint32_t));

	std::this_thread::sleep_for(std::chrono::milliseconds(200));

	/* Effectivement envoyer le segment. */
	socket.open(ip::udp::v4());
	socket.send_to(boost::asio::buffer(buffer), destEndpoint);
	socket.close();
}
